// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/base/network_interfaces_linux.h"

#include <memory>

#if !defined(OS_ANDROID)
#include <linux/ethtool.h>
#endif // !defined(OS_ANDROID)
#include <linux/if.h>
#include <linux/sockios.h>
#include <linux/wireless.h>
#include <set>
#include <sys/ioctl.h>
#include <sys/types.h>

#include "base/files/file_path.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_restrictions.h"
#include "net/base/address_tracker_linux.h"
#include "net/base/escape.h"
#include "net/base/ip_endpoint.h"
#include "net/base/net_errors.h"
#include "net/base/network_interfaces_posix.h"
#include "url/gurl.h"

namespace net {

namespace {

    // When returning true, the platform native IPv6 address attributes were
    // successfully converted to net IP address attributes. Otherwise, returning
    // false and the caller should drop the IP address which can't be used by the
    // application layer.
    bool TryConvertNativeToNetIPAttributes(int native_attributes,
        int* net_attributes)
    {
        // For Linux/ChromeOS/Android, we disallow addresses with attributes
        // IFA_F_OPTIMISTIC, IFA_F_DADFAILED, and IFA_F_TENTATIVE as these
        // are still progressing through duplicated address detection (DAD)
        // and shouldn't be used by the application layer until DAD process
        // is completed.
        if (native_attributes & (
#if !defined(OS_ANDROID)
                IFA_F_OPTIMISTIC | IFA_F_DADFAILED |
#endif // !OS_ANDROID
                IFA_F_TENTATIVE)) {
            return false;
        }

        if (native_attributes & IFA_F_TEMPORARY) {
            *net_attributes |= IP_ADDRESS_ATTRIBUTE_TEMPORARY;
        }

        if (native_attributes & IFA_F_DEPRECATED) {
            *net_attributes |= IP_ADDRESS_ATTRIBUTE_DEPRECATED;
        }

        return true;
    }

} // namespace

namespace internal {

    // Gets the connection type for interface |ifname| by checking for wireless
    // or ethtool extensions.
    NetworkChangeNotifier::ConnectionType GetInterfaceConnectionType(
        const std::string& ifname)
    {
        base::ScopedFD s(socket(AF_INET, SOCK_STREAM, 0));
        if (!s.is_valid())
            return NetworkChangeNotifier::CONNECTION_UNKNOWN;

        // Test wireless extensions for CONNECTION_WIFI
        struct iwreq pwrq = {};
        strncpy(pwrq.ifr_name, ifname.c_str(), IFNAMSIZ - 1);
        if (ioctl(s.get(), SIOCGIWNAME, &pwrq) != -1)
            return NetworkChangeNotifier::CONNECTION_WIFI;

#if !defined(OS_ANDROID)
        // Test ethtool for CONNECTION_ETHERNET
        struct ethtool_cmd ecmd = {};
        ecmd.cmd = ETHTOOL_GSET;
        struct ifreq ifr = {};
        ifr.ifr_data = &ecmd;
        strncpy(ifr.ifr_name, ifname.c_str(), IFNAMSIZ - 1);
        if (ioctl(s.get(), SIOCETHTOOL, &ifr) != -1)
            return NetworkChangeNotifier::CONNECTION_ETHERNET;
#endif // !defined(OS_ANDROID)

        return NetworkChangeNotifier::CONNECTION_UNKNOWN;
    }

    std::string GetInterfaceSSID(const std::string& ifname)
    {
        base::ScopedFD ioctl_socket(socket(AF_INET, SOCK_DGRAM, 0));
        if (!ioctl_socket.is_valid())
            return "";
        struct iwreq wreq = {};
        strncpy(wreq.ifr_name, ifname.c_str(), IFNAMSIZ - 1);

        char ssid[IW_ESSID_MAX_SIZE + 1] = { 0 };
        wreq.u.essid.pointer = ssid;
        wreq.u.essid.length = IW_ESSID_MAX_SIZE;
        if (ioctl(ioctl_socket.get(), SIOCGIWESSID, &wreq) != -1)
            return ssid;
        return "";
    }

    bool GetNetworkListImpl(
        NetworkInterfaceList* networks,
        int policy,
        const std::unordered_set<int>& online_links,
        const internal::AddressTrackerLinux::AddressMap& address_map,
        GetInterfaceNameFunction get_interface_name)
    {
        std::map<int, std::string> ifnames;

        for (internal::AddressTrackerLinux::AddressMap::const_iterator it = address_map.begin();
             it != address_map.end(); ++it) {
            // Ignore addresses whose links are not online.
            if (online_links.find(it->second.ifa_index) == online_links.end())
                continue;

            sockaddr_storage sock_addr;
            socklen_t sock_len = sizeof(sockaddr_storage);

            // Convert to sockaddr for next check.
            if (!IPEndPoint(it->first, 0)
                     .ToSockAddr(reinterpret_cast<sockaddr*>(&sock_addr), &sock_len)) {
                continue;
            }

            // Skip unspecified addresses (i.e. made of zeroes) and loopback addresses
            if (IsLoopbackOrUnspecifiedAddress(reinterpret_cast<sockaddr*>(&sock_addr)))
                continue;

            int ip_attributes = IP_ADDRESS_ATTRIBUTE_NONE;

            if (it->second.ifa_family == AF_INET6) {
                // Ignore addresses whose attributes are not actionable by
                // the application layer.
                if (!TryConvertNativeToNetIPAttributes(it->second.ifa_flags,
                        &ip_attributes))
                    continue;
            }

            // Find the name of this link.
            std::map<int, std::string>::const_iterator itname = ifnames.find(it->second.ifa_index);
            std::string ifname;
            if (itname == ifnames.end()) {
                char buffer[IFNAMSIZ] = { 0 };
                ifname.assign(get_interface_name(it->second.ifa_index, buffer));
                // Ignore addresses whose interface name can't be retrieved.
                if (ifname.empty())
                    continue;
                ifnames[it->second.ifa_index] = ifname;
            } else {
                ifname = itname->second;
            }

            // Based on the interface name and policy, determine whether we
            // should ignore it.
            if (ShouldIgnoreInterface(ifname, policy))
                continue;

            NetworkChangeNotifier::ConnectionType type = GetInterfaceConnectionType(ifname);

            networks->push_back(
                NetworkInterface(ifname, ifname, it->second.ifa_index, type, it->first,
                    it->second.ifa_prefixlen, ip_attributes));
        }

        return true;
    }

    std::string GetWifiSSIDFromInterfaceListInternal(
        const NetworkInterfaceList& interfaces,
        internal::GetInterfaceSSIDFunction get_interface_ssid)
    {
        std::string connected_ssid;
        for (size_t i = 0; i < interfaces.size(); ++i) {
            if (interfaces[i].type != NetworkChangeNotifier::CONNECTION_WIFI)
                return "";
            std::string ssid = get_interface_ssid(interfaces[i].name);
            if (i == 0) {
                connected_ssid = ssid;
            } else if (ssid != connected_ssid) {
                return "";
            }
        }
        return connected_ssid;
    }

} // namespace internal

bool GetNetworkList(NetworkInterfaceList* networks, int policy)
{
    if (networks == NULL)
        return false;

    internal::AddressTrackerLinux tracker;
    tracker.Init();

    return internal::GetNetworkListImpl(
        networks, policy, tracker.GetOnlineLinks(), tracker.GetAddressMap(),
        &internal::AddressTrackerLinux::GetInterfaceName);
}

std::string GetWifiSSID()
{
    NetworkInterfaceList networks;
    if (GetNetworkList(&networks, INCLUDE_HOST_SCOPE_VIRTUAL_INTERFACES)) {
        return internal::GetWifiSSIDFromInterfaceListInternal(
            networks, internal::GetInterfaceSSID);
    }
    return "";
}

} // namespace net
